/*
 * Copyright (c) 2010-2013 Ruben Chakhmakhchyan. All rights reserved.
 * 
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Eclipse Public License
 * Version 1.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://opensource.org/licenses/EPL-1.0 and read it before using this file.
 */

#include "plugin_model_impl_common.h"


void toolCallback( AsyncInvokeArg* arg );


void _PluginModel::alignMousePos( long& x, long& y )
{
    if ( x < m_viewPortionPos.x || ( m_snapping && ( x - m_viewPortionPos.x ) < 4 ) )
        x = m_viewPortionPos.x;
    long right = m_viewPortionRect.getRight() + m_viewPortionPos.x;
    if ( x > right || ( m_snapping && ( right - x ) < 4 ) )
        x = right;
    if ( y < m_viewPortionPos.y || ( m_snapping && ( y - m_viewPortionPos.y ) < 4 ) )
        y = m_viewPortionPos.y;
    long bottom = m_viewPortionRect.getBottom() + m_viewPortionPos.y;
    if ( y > bottom || ( m_snapping && ( bottom - y ) < 4 ) )
        y = bottom;
}

bool _PluginModel::crop()
{
    if ( m_cropState && m_cropRectDefined )
    {
        this->crop( m_cropRect.left, m_cropRect.top, m_cropRect.width, m_cropRect.height );
    }

    return true;
}

bool _PluginModel::crop( int left, int top, int width, int height )
{
    if ( !hasImage() || width <= 0 || height <= 0 ) return false;

    // check bounds
    if ( left < 0 ) left = 0;
    if ( top < 0 ) top = 0;
    if ( width > ( m_imageSize.width - left ) ) width = m_imageSize.width - left;
    if ( height > ( m_imageSize.height - top ) ) height = m_imageSize.height - top;

    m_activeImage->lock();

    const Pixels* pixels = m_activeImage->getPixels();
    const Pixels& p = *pixels;
    byte_t* ibits = p.pixels;

    long nwidthBits = width * p.bitsPerPixel;
    long nstride = ( ( ( nwidthBits + 7 ) & ~7 ) >> 3 );
    uint_t nsizeImage = nstride * height;

    byte_t* pDestPixels = ( byte_t* ) ::malloc( nsizeImage );
    ::memset( pDestPixels, 0, nsizeImage );

    byte_t* nbits = pDestPixels;

    // copy the content to the new image
    byte_t* sp, *dp;
    int colorBytes = p.bitsPerPixel >> 3;

    if ( colorBytes )
    {
        for ( int i = 0; i < height; i++ )
        {
            sp = ibits + ( top + i ) * p.rowStride + left * colorBytes;
            dp = nbits + i * nstride;
            ::memcpy( dp, sp, width * colorBytes );
        }
    }
    else
    {
        for ( int j = 0; j < height; j++ )
        {
            sp = ibits + ( top + j ) * p.rowStride;
            dp = nbits + j * nstride;

            for ( int i = 0; i < width; i++ )
            {
                byte_t b = ::getBit( sp, left + i, false );

                uint_t byteNumber = ( i >> 3 );
                byte_t* np = dp + byteNumber;
                byte_t bit = i % 8;

                *np = *np | ( b << ( 7 - bit ) );
            }
        }
    }

    setCropState( false );

    // set the new image

    Pixels npixels = Pixels( p );

    npixels.width = width;
    npixels.height = height;
    npixels.pixels = pDestPixels;
    npixels.length = nsizeImage;
    npixels.rowStride = nstride;

    m_activeImage->unlock();

    setImage( npixels, false );

    requestUpdateView();

    return true;
}

void _PluginModel::startMouseCrop( long xpos, long ypos )
{
    if ( m_cropRectDefined )
    {
        int threshold = 4;

        Rect windowCropRect = { m_cropRectWindowPos.x, m_cropRectWindowPos.y, m_cropRectSize.width, m_cropRectSize.height };

        if ( pointInRect( windowCropRect, xpos, ypos, threshold ) )
        {
            m_mouseCrop = true;
            m_mouseCropDragging = true;

            m_cropRectStartPos = windowCropRect.pos;
        }
        else
        {
            if ( m_cropSide = pointOnSide( windowCropRect, xpos, ypos, threshold ) )
            {
                switch( m_cropSide )
                {
                case 1:
                case 3:
                    ::SetCursor( ::LoadCursor( NULL, IDC_SIZENS ) );
                    break;
                case 2:
                case 4:
                    ::SetCursor( ::LoadCursor( NULL, IDC_SIZEWE ) );
                    break;
                }

                m_mouseCrop = true;
                m_mouseCropRefine = true;
            }
            else if ( m_cropCorner = pointInCorner( windowCropRect, xpos, ypos, 5 ) )
            {
                switch( m_cropCorner )
                {
                case 1:
                case 3:
                    ::SetCursor( ::LoadCursor( NULL, IDC_SIZENWSE ) );
                    break;
                case 2:
                case 4:
                    ::SetCursor( ::LoadCursor( NULL, IDC_SIZENESW ) );
                    break;
                }

                m_mouseCrop = true;
                m_mouseCropRefine = true;
            }
        }
    }
    else
    {
        m_mouseCrop = true;
    }

    if ( m_mouseCrop )
    {
        alignMousePos( xpos, ypos );

        Point point = { xpos, ypos };
        m_mouseTopPos = m_mouseStartPos = m_mousePos = point;
        m_mouseRectInitialized = false;
    }
}

void _PluginModel::stopMouseCrop( long xpos, long ypos )
{
    if ( m_mouseCrop )
    {
        m_mouseCrop = false;

        if ( !m_mouseCropDragging && !m_mouseCropRefine )
        {
            alignMousePos( xpos, ypos );

            bool valid = m_mouseTopPos.x - m_mouseStartPos.x >= 32 && m_mouseTopPos.y - m_mouseStartPos.y >= 32;

            if ( !m_cropRectDefined && valid )
            {
                m_cropRectDefined = true;

                m_cropRectWindowPos.x = m_mouseStartPos.x;
                m_cropRectWindowPos.y = m_mouseStartPos.y;

                m_cropRectSize.width = m_mouseTopPos.x - m_mouseStartPos.x;
                m_cropRectSize.height = m_mouseTopPos.y - m_mouseStartPos.y;

                m_cropRectViewPos.x = m_cropRectWindowPos.x + m_viewPortionRect.left - m_viewPortionPos.x;
                m_cropRectViewPos.y = m_cropRectWindowPos.y + m_viewPortionRect.top - m_viewPortionPos.y;

                m_cropRect.left = ::lround( m_cropRectViewPos.x * m_inverseZoomFactor );
                m_cropRect.top = ::lround( m_cropRectViewPos.y * m_inverseZoomFactor );
                m_cropRect.width = ::lround( m_cropRectSize.width * m_inverseZoomFactor );
                m_cropRect.height = ::lround( m_cropRectSize.height * m_inverseZoomFactor );
            }
            else
            {
                resetCrop();
            }
            requestUpdateView();
        }
        else
        {
            m_mouseCropDragging = false;
            m_mouseCropRefine = false;

            ::SetCursor( ::LoadCursor( NULL, IDC_ARROW ) );
        }
    }
}

void _PluginModel::resetCrop()
{
    Rect er = { 0, 0, 0, 0 };
    Point ep = { 0, 0 };
    Size es = { 0, 0 };

    m_cropRect = er;
    m_cropRectWindowPos = ep;
    m_cropRectViewPos = ep;
    m_cropRectSize = es;

    m_mouseStartPos = ep;
    m_mousePos = ep;
    m_mouseTopPos = ep;
}

void _PluginModel::doMouseCrop( long xpos, long ypos )
{
    if ( m_mouseCrop )
    {
        if ( !m_mouseCropDragging && !m_mouseCropRefine )
        {
            alignMousePos( xpos, ypos );

            m_mouseTopPos.x = xpos;
            m_mouseTopPos.y = ypos;

            m_mouseZooming = true;

            requestUpdateView();
        }
        else
        {
            bool refresh = false, refresh1 = false, refresh2 = false;

            long boundLeft = m_viewPortionPos.x - m_viewPortionRect.left;
            long boundRight = boundLeft + m_viewSize.width - 1;
            long boundTop = m_viewPortionPos.y - m_viewPortionRect.top;
            long boundBottom = boundTop + m_viewSize.height - 1;

            long cropRectWidth = m_cropRectSize.width;
            long cropRectHeight = m_cropRectSize.height;
            long cropRectRight = m_cropRectWindowPos.x + cropRectWidth;
            long cropRectBottom = m_cropRectWindowPos.y + cropRectHeight;

            long originX = m_mouseStartPos.x - m_cropRectStartPos.x; // coordinate of mouse start point relative to crop rect
            long originY = m_mouseStartPos.y - m_cropRectStartPos.y; // coordinate of mouse start point relative to crop rect

            //Size cropRectSize = m_cropRectSize;
            Point crwp = m_cropRectWindowPos;

            if ( m_mouseCropDragging )
            {
                refresh1 = refresh2 = true;

                // check if the crop rect is within image bounds

                // left
                if ( xpos < boundLeft + originX )
                {
                    if ( crwp.x == boundLeft )
                        refresh1 = false;
                    else
                        crwp.x = boundLeft;
                }
                // right
                else if ( xpos > boundRight - ( cropRectWidth - originX ) )
                {
                    if ( cropRectRight == boundRight )
                        refresh1 = false;
                    else
                        crwp.x = boundRight - cropRectWidth;
                }
                else
                {
                    crwp.x += xpos - m_mousePos.x;
                }

                // top
                if ( ypos < boundTop + originY )
                {
                    if ( crwp.y == boundTop )
                        refresh2 = false;
                    else
                        crwp.y = boundTop;
                }
                // bottom
                else if ( ypos > boundBottom - ( cropRectHeight - originY ) )
                {
                    if ( cropRectBottom == boundBottom )
                        refresh2 = false;
                    else
                        crwp.y = boundBottom - cropRectHeight;
                }
                else
                {
                    crwp.y += ypos - m_mousePos.y;
                }

                // save current position
                m_mousePos.x = xpos;
                m_mousePos.y = ypos;


                /*if ( refresh1 || refresh2 )
                {
                m_mouseZooming = false;

                requestUpdateView();
                }*/
            }
            else if ( m_mouseCropRefine )
            {
                //bool refresh = false;

                // check if the rect has its minimal size
                if ( cropRectWidth < 32 )
                {
                    cropRectWidth = 32;
                    refresh = true;
                }
                if ( cropRectHeight < 32 )
                {
                    cropRectHeight =  32;
                    refresh = true;
                }

                if ( !refresh )
                {
                    int dx = xpos - m_mousePos.x;
                    int dy = ypos - m_mousePos.y;

                    // check if the crop rect is out of bounds

                    int dc = 0;	// identifies the snapping distance
                    if ( xpos < boundLeft )
                    {
                        dc = crwp.x - boundLeft;
                        if ( dc != 0 )
                        {
                            crwp.x = boundLeft;
                            m_cropRectSize.width += dc;
                        }
                        dx = 0;
                    }
                    else if ( xpos > boundRight )
                    {
                        dc = boundRight - cropRectRight;
                        if ( dc != 0 )
                        {
                            m_cropRectSize.width += dc;
                        }
                        dx = 0;
                    }
                    if ( ypos < m_viewPortionRect.top )
                    {
                        dc = crwp.y - boundTop;
                        if ( dc != 0 )
                        {
                            crwp.y = boundTop;
                            m_cropRectSize.height += dc;
                        }
                        dy = 0;
                    }
                    else if ( ypos > boundBottom )
                    {
                        dc = boundBottom - cropRectBottom;
                        if ( dc != 0 )
                        {
                            m_cropRectSize.height += dc;
                        }
                        dy = 0;
                    }

                    if ( dc )
                        refresh = true;

                    if ( m_cropSide )
                    {
                        int d = ( m_cropSide % 2 ) ? dy : dx;
                        if ( d != 0 )
                        {
                            switch ( m_cropSide )
                            {
                            case 1:
                                crwp.y += d;
                                m_cropRectSize.height -= d;
                                break;
                            case 2:
                                m_cropRectSize.width += d;
                                break;
                            case 3:
                                m_cropRectSize.height += d;
                                break;
                            case 4:
                                crwp.x += d;
                                m_cropRectSize.width -= d;
                                break;
                            }

                            refresh = true;
                        }
                    }
                    else if ( m_cropCorner )
                    {
                        if ( dx )
                        {
                            switch( m_cropCorner )
                            {
                            case 1:
                            case 4:
                                crwp.x += dx;
                                m_cropRectSize.width -= dx;
                                break;
                            case 2:
                            case 3:
                                m_cropRectSize.width += dx;
                                break;
                            }
                        }
                        if ( dy )
                        {
                            switch( m_cropCorner )
                            {
                            case 1:
                            case 2:
                                crwp.y += dy;
                                m_cropRectSize.height -= dy;
                                break;
                            case 3:
                            case 4:
                                m_cropRectSize.height += dy;
                                break;
                            }
                        }
                        if ( dx || dy )
                        {
                            refresh = true;
                        }
                    }
                }

                m_mousePos.x = xpos;
                m_mousePos.y = ypos;

                /*if ( refresh )
                {
                m_mouseZooming = false;

                requestUpdateView();
                }*/
            }

            m_cropRectWindowPos = crwp;

            m_cropRectViewPos.x = m_cropRectWindowPos.x - m_viewPortionPos.x + m_viewPortionRect.left;
            m_cropRectViewPos.y = m_cropRectWindowPos.y - m_viewPortionPos.y + m_viewPortionRect.top;

            m_cropRect.left = ::lround( m_cropRectViewPos.x * m_inverseZoomFactor );
            m_cropRect.top = ::lround( m_cropRectViewPos.y * m_inverseZoomFactor );
            m_cropRect.width = ::lround( m_cropRectSize.width * m_inverseZoomFactor );
            m_cropRect.height = ::lround( m_cropRectSize.height * m_inverseZoomFactor );

            if ( refresh || refresh1 || refresh2 )
            {
                m_mouseZooming = false;

                requestUpdateView();
            }
        }
    }
}

void _PluginModel::startMousePan( long xpos, long ypos )
{
    m_mousePan = true;
    m_mousePos.x = xpos;
    m_mousePos.y = ypos;
}

void _PluginModel::stopMousePan()
{
    m_mousePan = false;
}

void _PluginModel::doMousePan( long xpos, long ypos )
{
    if ( m_mousePan || m_spacePressed )
    {
        int dx = xpos - m_mousePos.x;
        int dy = ypos - m_mousePos.y;

        bool handledX = panX( dx, false );
        bool handledY = panY( dy, false );

        m_mousePos.x = xpos;
        m_mousePos.y = ypos;

        if ( handledX || handledY )
        {
            requestUpdateView();
        }
    }
}

void _PluginModel::startMouseZoom( long xpos, long ypos )
{
    m_mouseZoom = true;

    alignMousePos( xpos, ypos );

    Point point = { xpos, ypos };
    m_mouseStartPos = m_mousePos = point;

    m_mouseRectInitialized = false;
}

void _PluginModel::stopMouseZoom( long xpos, long ypos )
{
    m_mouseZoom = false;

    alignMousePos( xpos, ypos );

    long left, top, width, height;

    xpos = xpos >= 0 ? xpos : 0;
    ypos = ypos >= 0 ? ypos : 0;

    left = m_mouseStartPos.x < xpos ? m_mouseStartPos.x : xpos;
    top = m_mouseStartPos.y < ypos ? m_mouseStartPos.y : ypos;
    width = abs( xpos - m_mouseStartPos.x );
    height = abs( ypos - m_mouseStartPos.y );

    long viewX = left < m_viewPortionPos.x ? 0 : left - m_viewPortionPos.x + m_viewPortionRect.left;
    long viewY = top < m_viewPortionPos.y ? 0 : top - m_viewPortionPos.y + m_viewPortionRect.top;

    bool simpleZoom = width < 5 && height < 5; // if the area too small for regional zoom

    double df = simpleZoom ?  1.5f : ( width > height ? ( m_viewportSize.width / ( double ) width ) : ( m_viewportSize.height / ( double ) height ) );

    double zoomFactor = m_zoomFactor;	// current zoom factor

    setZoomFactor( zoomFactor * df );

    df = m_zoomFactor / zoomFactor;		// calculate df based on zoom factor limit

    if ( simpleZoom )
    {
        m_viewPortionRect.left = ( long ) ( viewX * df - ( m_viewPortionRect.width >> 1 ) + m_viewPortionPos.x );
        m_viewPortionRect.top = ( long ) ( viewY * df - ( m_viewPortionRect.height >> 1 ) + m_viewPortionPos.y );
    }
    else
    {
        m_viewPortionRect.left = ( long ) ( viewX * df );
        m_viewPortionRect.top = ( long ) ( viewY * df );
    }

    if ( m_viewPortionRect.left < 0 ) m_viewPortionRect.left = 0;
    else if ( m_viewPortionRect.left > m_viewSize.width - m_viewPortionRect.width ) m_viewPortionRect.left = m_viewSize.width - m_viewPortionRect.width;

    if ( m_viewPortionRect.top < 0 ) m_viewPortionRect.top = 0;
    else if ( m_viewPortionRect.top > m_viewSize.height - m_viewPortionRect.height ) m_viewPortionRect.top = m_viewSize.height - m_viewPortionRect.height;

    // refresh view
    m_refresh = true;

    requestUpdateView();
}

void _PluginModel::doMouseZoom( long xpos, long ypos )
{
    if ( m_mouseZoom )
    {
        alignMousePos( xpos, ypos );

        m_mouseTopPos.x = xpos;
        m_mouseTopPos.y = ypos;

        m_mouseZooming = true;

        requestUpdateView();
    }
}

bool _PluginModel::panX( int delta, bool updateView )
{
    bool handled = false;

    int mx = m_viewSize.width - m_viewportSize.width;
    long sx = m_viewPortionRect.left;

    if ( ( delta < 0 && m_viewPortionRect.left < mx || m_viewPortionPos.x == 0 ) || ( delta > 0 && m_viewPortionRect.left >= 0 ) )
    {
        int dx = m_viewPortionRect.left - delta;
        if ( dx < 0 )
        {
            m_viewPortionRect.left = 0;
        }
        else if ( dx > mx )
        {
            m_viewPortionRect.left = mx;
        }
        else
            m_viewPortionRect.left = dx;

        m_cropRectWindowPos.x += sx - m_viewPortionRect.left;

        if ( updateView )
        {
            requestUpdateView();
        }

        handled = true;
    }

    return handled;
}

bool _PluginModel::panY( int delta, bool updateView )
{
    bool handled = false;

    int my = m_viewSize.height - m_viewportSize.height;
    long sy = m_viewPortionRect.top;

    if ( ( delta < 0 && m_viewPortionRect.top < my ) || ( delta > 0 && m_viewPortionRect.top >= 0 ) )
    {
        int dy = m_viewPortionRect.top - delta;
        if ( dy < 0 )
        {
            m_viewPortionRect.top = 0;
        }
        else if ( dy > my )
        {
            m_viewPortionRect.top = my;
        }
        else
            m_viewPortionRect.top = dy;

        m_cropRectWindowPos.y += sy - m_viewPortionRect.top;

        if ( updateView )
        {
            requestUpdateView();
        }

        handled = true;
    }
    return handled;
}

bool _PluginModel::pan( int dx, int dy )
{
    bool handledX = panX( dx, false );
    bool handledY = panY( dy, false );

    if ( handledX || handledY )
    {
        requestUpdateView();
    }

    return true;
}

void _PluginModel::setZoomState( bool value, bool isInternal )
{
    if ( m_zoomState = value )
    {
        setCropState( false );
    }

    if ( isInternal )
    {
        postCallback( new ToolStateAsyncArg( this, 1, value ), toolCallback );
    }
}

void _PluginModel::setPanState( bool value, bool isInternal )
{
    m_panState = value;

    if ( isInternal )
    {
        postCallback( new ToolStateAsyncArg( this, 2, value ), toolCallback );
    }
}

void _PluginModel::setCropState( bool value, bool isInternal )
{
    if ( m_cropState = value )
    {
        m_cropRectDefined = false;

        resetCrop();

        setZoomState( false );
    }
    else
    {
        requestUpdateView();
    }

    if ( isInternal )
    {
        postCallback( new ToolStateAsyncArg( this, 3, value ), toolCallback );
    }
}

void toolCallback( AsyncInvokeArg* arg )
{
    ToolStateAsyncArg* toolStateArg = ( ToolStateAsyncArg* ) arg;

    // tool state changed event
    arg->getPluginModel()->getPlugin()->onToolStateChanged( toolStateArg->getToolId(), toolStateArg->getToolState() ? 1 : 0 );
}

bool _PluginModel::rotate( int angle )
{
    if ( !hasImage() ) return false;

    if ( angle % 90 != 0 && angle == 0 )
        return false;

    const Pixels* pixels = m_activeImage->getPixels();
    const Pixels& p = *pixels;

    int degrees = angle % 360;
    if ( degrees < 0 ) degrees += 360;

    int iheight = p.height;
    int iwidth = p.width;
    int iwidthBits = iwidth * p.bitsPerPixel;
    int istride = p.rowStride;
    int ids = istride - ( iwidthBits >> 3 );

    unsigned int colors = 0;

    byte_t* ibits = p.pixels;

    int nwidth = iheight;
    int nheight = iwidth;

    int nwidthBits = nwidth * p.bitsPerPixel;
    int nstride = ( ( ( nwidthBits + 7 ) & ( ~7 ) ) >> 3 );
    int nds = nstride - ( nwidthBits >> 3 );
    uint_t nsizeImage = nstride * nheight;

    byte_t* pDestPixels = new byte_t[ nsizeImage ];
    if ( p.bitsPerPixel == 1 ) // set all the bits to 0 for the binary | to work properly
        ::memset( pDestPixels, 0, nsizeImage );

    byte_t* nbits = pDestPixels;

    int in = 0, jn = 0;
    unsigned char* np = 0;
    int colorBytes = p.bitsPerPixel >> 3;

    if ( colorBytes )
    {
        // the normal transformation should be
        // 270:	( i, j ) -> ( j, u - i + 1 )
        // 90:	( i, j ) -> ( v - j + 1, i )

        byte_t* line = ibits;

        for ( int j = 0; j < iheight; j++ )
        {
            byte_t* lp = line;

            for ( int i = 0; i < iwidth; i++ )
            {
                switch ( degrees )
                {
                case 90:
                    in = iheight - j - 1;
                    jn = i;
                    break;
                case 270:
                    in = j;
                    jn = iwidth - i - 1;
                    break;
                }

                np = nbits + ( jn * nstride + in * colorBytes );

                ::memcpy( np, lp, colorBytes );

                lp += colorBytes;
            }
            line += istride;
        }
    }
    else
    {
        byte_t* line = ibits;

        for ( int j = 0; j < iheight; j++ )
        {
            byte_t* lp = line;

            for ( int i = 0; i < iwidth; i++ )
            {
                switch ( degrees )
                {
                case 90:
                    in = iheight - j - 1;
                    jn = i;
                    break;
                case 270:
                    in = j;
                    jn = iwidth - i - 1;
                    break;
                }

                uint_t lineBase = jn * nstride;
                uint_t byteNumber = lineBase + ( in >> 3 );
                np = nbits + byteNumber;
                byte_t bit = in % 8;
                byte_t ibit = i % 8;

                byte_t byte = ( ( *lp ) &  ( 1 << ( 7 - ibit ) ) );

                *np = *np | ( ( byte >> ( 7 - ibit ) ) << ( 7 - bit ) );

                if ( ibit == 7 )
                {
                    lp++;
                }
            }
            line += istride;
        }
    }

    Pixels npixels = Pixels( p );

    npixels.width = nwidth;
    npixels.height = nheight;
    npixels.pixels = pDestPixels;
    npixels.length = nsizeImage;
    npixels.rowStride = nstride;

    setImage( npixels, false );

    requestUpdateView();

    return true;
}

InteractiveTool _PluginModel::getCurrentTool()
{
    if ( m_panState || m_spacePressed )
    {
        return InteractiveTool::Pan;
    }
    else if ( m_zoomState )
    {
        return InteractiveTool::Zoom;
    }
    else if ( m_cropState )
    {
        return InteractiveTool::Crop;
    }

    return InteractiveTool::None;
}
